/** @global */
injectStore = require('ee-vuex').injectStore;
/**
 * @global
 * vue组件有以下问题
 * 1. 例如radio，未绑定v-model时，无法用v-model和value进行比较来计算出checked属性
 *    即没有v-model就用不了，对props设置required:true也只是输出警告，无法纠正错误
 * 2. 再例如van-checkbox-group，v-model是一个数组，绑定null时，直接报错导致页面空白
 *    原因是要求数组必须有值，type:Array | required:true | default:()=>[]都验证通过
 *    即便加上validator(value) { return !!value; }也只是输出警告，无法纠正错误
 * 
 * 综上所述，ui框架组件存在以下确定
 * 1. 缺少绑定某个props可能会导致组件不可用的情况(不好用)
 * 2. 绑定了错误的值会导致白屏(请求后端数据时，就是运行时错误)
 * 
 * props注意特性
 * 1. validator(value)可以在每次prop赋值时进行检测
 *    设置了type检查且错误时，不会进入validator
 *    default赋值也会进入validator
 * 2. default值是个只读值(没有proxy)，如果是个数组，引用它的computed属性不会随着数组元素变化而改变
 *    v-model绑定了undefined，default的值也不会同步给v-model，即默认状态v-model和modelValue不一致
 * 3. props组件内部不能赋值，父组件动态绑定则可以修改值，所以对于值变化需要额外注意
 * 4. modelValue如果通过update:modelValue对外赋值，万一外部没有绑定v-model则导致modelValue一直不会变
 * 5. props首次绑定值是在created阶段，此时dom还没渲染完成，watch需要加上immediate:true
 *    watch加了immediate:true，绑定的undefined时，即value和oldValue都是undefined，watch依然会触发
 * 
 * 综上所述，设计一个更适用于实际开发和使用的props系统，力求解决以下问题
 * 1. 组件内外都可以赋值(外部不赋值内部依旧可用)
 * 2. 对于错误的值可以纠正(例如要求非空，可以判空后对自己赋值成正确的值)
 * 3. 首次赋值是在mounted阶段，dom元素已经就绪
 * 4. 默认对组件实现了watch和update:prop，仅需要关注set函数
 * 5. 兼容props原有的写法，增加prop: (value) => {}写法
 * 
 * 使用如下
 * 1. 导出组件时调用方法export default ee({})
 * 2. 配置prop，原有属性依旧生效，增加set属性
 * props: {
 *   modelValue: {
 *     type: Array,
 *     required: true,
 *     default: () => [],
 *     validator(value) { return true; }
 *     // 这里是新增内容
 *     get: (value) {
 *       // 不写get默认返回生成的__modelValue
 *       // 假设类型是[String, Number, Function]的prop，外部赋值如下
 *       // String: modelValue="1"
 *       // Number: :modelValue="1"
 *       // Function: :modelValue="i => i.checked ? 2 : 3"
 *       // 组件内部绑定prop时，无法兼容Function的写法
 *       // 此时会用这个get方法生成一个computed的_modelValue
 *       // 组件就可以绑定这个_modelValue了，get方法实现如下
 *       // return value?.constructor == Function ? value(this) : value;
 *     },
 *     set: (value, oldValue) {
 *       // 这里实现你对当前属性赋值的操作
 *     }
 *   }
 * }
 * 对于新设计，默认值可选，set必选，没有set则视为原生prop
 * type | required | validator都是输出警告，并不能实际解决问题
 * 所以针对最有用的默认值和set，增加以下简单写法
 * modelValue: function(value[ = default]) {},
 * 新设计再增加get的用法，有set时get默认返回__prop，get的简单写法如下
 * modelValue: function() {} (比起set就是不带参数，但任然可以使用value变量拿到prop的当前值)
 * 
 * 实现过程如下
 * 1. 组件脚本就是个对象，调用ee({})对新语法对象改写成vue原生对象
 * 2. 对于一个新语法的prop，定义了prop
 * 3. data增加了一个__prop变量
 * 4. emits增加了update:prop
 * 5. computed增加了一个prop变量，get返回__prop，set就是新语法定义的set
 *    其中默认实现了对__prop的赋值和emit("update:prop")的调用
 * 6. watch增加了对props.prop和computed.prop的检测(没有immediate)，有变化时调用computed.prop.set
 * 7. mounted增加了computed.prop = props.prop
 * (由于props和computed不能重名，所以computed的属性叫_prop)
 * 
 * 这样，无论是
 * 父组件动态绑定的prop改变，
 * 或者组件内部emit导致model的改变，
 * 或者组件内部set导致prop的改变，
 * 都统一调用的set方法且不会重复调用
 * 
 * 方法参数带默认值的，调用方法传入undefined，参数任然会是默认值
 * (P.S. 参数改为ref后不能带默认值了，这个设定暂时用不了)
 * 
 * 2023/04/18
 * data, props, computed最新测试，3个对象里都定义了同名参数时
 * 1. 模板中读取会调用computed的get
 * 2. 模板中赋值会调用computed的set，JS中赋值会赋值给data里的变量
 */
injectRender = function (obj) {
  // 组件注册仅会执行一次，创建组件多个实例不会多次调用此方法

  // eerender渲染注入，这样注入主要是为了解决开发阶段webpack会直接替换render方法导致之前的注入失效的问题
  if (!obj.mixins) obj.mixins = [];
  const renderMixin = {
    beforeCreate() {
      eerender(this);
      // 注入完渲染后删掉注入的mixin
      obj.mixins.remove(renderMixin);
    },
  };
  obj.mixins.push(renderMixin);

  // 下面已经改为ee-vuex.injectStore
  return obj;

  // 没有定义props的组件跳过
  if (!obj.props)
    return obj;
  // props: []的写法
  if (obj.props.length != undefined)
    return obj;

  let get, set;
  let sets = {};
  for (let key in obj.props) {
    let value = obj.props[key];
    if (value.constructor == Object) {
      // 没有定义set为默认语法不处理
      if (!value.set && !value.get)
        continue;
      get = value.get;
      set = value.set;
    } else if (value.constructor == Function) {
      // prop: Array 等只写一个类型的情况，类型也是Function需要跳过
      // prop: function nickname(value) {} 的情况极为特殊，就当是错误吧
      if (value.name != key)
        continue;
      // 没有参数为get，有参数为set
      if (value.length) {
        get = undefined;
        set = value;
      } else {
        get = value;
        set = undefined;
      }

      obj.props[key] = {};
    } else continue;

    if (get?.constructor != Function && set?.constructor != Function)
      throw "props get/set must be a function";

    if (!get)
      get = function () { return this["__" + key]; }

    sets[key] = { get, set };
  }

  if (get || set) {
    let data = {};
    let mixin = {
      data() {
        let ret = {};
        for (let item in data) {
          let value = data[item];
          // 对于数组，对象的默认值，一般会是一个方法返回一个新实例，此时避免不了mounted赋值
          // P.S. webpack中babel用了es2015的编译规则，箭头函数中的this会被直接编译成undefined
          // 若要使用方法且用到this，需要写成default: function() { return this.xxx }
          if (value?.constructor == Function)
            value = value.call(this);
          ret[item] = value;
        }
        return ret;
      },
      emits: [],
      computed: {},
      watch: {},
      mounted() {
        // 对所有computed属性赋值上prop的值
        for (let key in sets)
          if (sets[key].set && this[key] !== undefined)
            this["_" + key] = this[key];
      }
    };
    for (let key in sets) {
      // 对于设置了默认值的，__key设置成默认值，这属于组件内部定义的值，可以不用触发变化
      // 和调用组件不一样，调用组件绑定的prop，属于外部赋值，需要触发变化
      // 所以还是和原本props不一样，原本props外部绑定也会被视为默认值不会触发变化
      // get only的情况下，__prop不赋值default，让prop保留默认值，好在get计算后赋值给__prop
      if (sets[key].set) {
        let defaultValue = obj.props[key].default;
        if (defaultValue == undefined && obj.props[key].type == Boolean)
          defaultValue = false;
        delete obj.props[key].default;
        data["__" + key] = defaultValue;
      } else {
        data["__" + key] = undefined;
      }
      mixin.computed["_" + key] = {
        get: sets[key].get,
        set: !sets[key].set || function (value) {
          // 值不变不调用 watch为===判断，watch到变化肯定会调用
          if (value == this["__" + key])
            return;
          this["__" + key] = value;
          // 若call(this)效率低下，可以考虑将原始set方法注入methods中，这里this[__key]调用即可
          // 内部要改变值，自行赋值this.__key = value = newValue
          sets[key].set.call(this, value);
          this.$emit("update:" + key, this["__" + key]);
        }
      };
      if (sets[key].set) {
        mixin.emits.push("update:" + key);
        mixin.watch[key] = function (value, ov) {
          // 内部改变：对computed赋值触发emit引起外部改变，prop和computed已经相等
          // 外部改变：父组件绑定的动态变量改变，prop和computed不等
          // 只有prop和computed不等时才赋值
          if (value != this["__" + key])
            this["_" + key] = value;
        };
      }
    }
    if (!obj.mixins) obj.mixins = [];
    obj.mixins.push(mixin);
  }
  return obj;
}

ps = { type: String }
pn = { type: [String, Number] }
pb = { type: [String, Boolean] }
pt = { type: Boolean, default: true }
pa = { type: Array, default: () => [] }
po = { type: Object, default: () => { } }
pd = { type: Date, default: () => new Date() }
pf = { type: Function }
pp = { type: [Function, Promise] }
ep = function (value) {
  return { default: value };
}
epf = function (value, key) {
  return {
    default: value,
    get() {
      let v = this[key];
      if (v?.call) {
        if (this["__" + key])
          return this["__" + key];
        let result = v(this);
        if (result?.constructor == Promise)
          result.then(ret => this["__" + key] = ret)
        else
          return result;
        if (this["__" + key])
          return this["__" + key];
        else
          return v;
      }
      else
        return v;
    }
  }
}

/** 向组件render方法注入参数或子对象
 * 使用场景
 *   基本是组件和另外一个组件需要同级，且让组件本身继承$attrs，两个组件要共同存在
 *   例如placeholder占位div，overlay遮罩层，都是要与自定义组件共存
 * 
 * 参数注入：例如出现动画注入，设置出现位置，对组件的根元素注入class
 * 1. 子组件(假设child)继承或混合ee-position
 * 2. 业务场景使用子组件<child position="top"></child>
 * 3. ee-position内部向子组件根元素插入class
 * 4. 渲染结果<child class="eet move top in"></child>
 * P.S. 原本使用this.$el.classList.add插入class也可以做到，但是业务场景绑定了:class="i ? 'c1' : 'c2'"，在i切换时，插入的class会消失
 * 
 * 插入组件：例如抽象父组件placeholder，用于fixed层的占位
 * 1. 子组件(假设child)继承或混合placeholder
 * 2. 业务场景使用子组件<child placeholder></child>
 * 3. placeholder内部根据是否显示来向子组件的el前插入占位div
 * 4. 渲染结果<div></div><child></child>
 * P.S. 原本使用this.$el.parentElement.insertBefore插入DOM元素也可以做到，但是使用注入渲染可以使用组件而不仅仅是DOM
 * 
 * @param component vue组件或组件的实例，自己传组件实例请在beforeCreate或beforeMount
 * @param render2(vnode: VNode) 子组件渲染出VNode，可用于修改组件的props或者添加/删除子组件
 * 
 * 使用方法：
 * @example 1. 使用injectRender，注入$options.render2
 * export default injectRender({
 *  render2(vnode) {
 *   console.log("渲染注入 this.$el渲染出来的VNode: ", vnode)
 *   // 修改props，但是不要直接修改props对象里面的字段，多个组件渲染会用的同一个props实例
 *   vnode.props = { ...vnode.props }
 *   vnode.props.class = `myclass ${jsclass} ` + (vnode.props.class || "");
 *   // 注入子组件
 *   vnode.children.push(h(子组件));
 *   // 修改props方法2，直接使用import { cloneVNode } from 'vue'修改props，return新的vnode出去即可
 *   return cloneVNode(vnode, { class: `myclass ${jsclass}` })
 *  }
 * })
 * @example 2. 手动调用eerender注入，注意function(vnode)可以让方法内部this获取当前组件实例，而不要使用()=>
 * export default {
 *  beforeCreate() {
 *   eerender(this, function(vnode) { })
 *  }
 * }
 */
function eerender(component, render2) {
  const instance = component.__v_skip ? component : undefined;
  // 组件实例
  if (component.__v_skip)
    component = component.$.type;
  // 运行时不要重复注入渲染方法
  if (component.mixinrender)
    return;

  // 循环检查extends和mixins里的注入函数，优先extends，mixins按顺序全部注入函数都需要执行
  const render2queue = [];
  componentWalker(component, i => i.render2 ? render2queue.push(i.render2) : '')
  if (render2)
    render2queue.push(render2);
  if (!render2queue.length)
    return;

  if (render2queue.length > 1) {
    render2 = function (vnode) {
      for (const r2 of render2queue) {
        const newnode = r2.call(this, vnode);
        if (newnode)
          vnode = newnode;
      }
      return vnode;
    }
  } else
    render2 = render2queue[0];

  // 缺少<template>的vue文件，render函数为空
  const render = component.render;

  component.mixinrender = true;
  component.render = function () {
    let vnode;
    // 缺少<template>的vue文件，render函数为空
    if (render)
      vnode = render.apply(arguments[0], arguments);

    // P.S. 组件根节点不止一个，渲染时会用一个Fragment(仅渲染内部子组件不渲染自身)节点包住所有子节点
    // 渲染弹窗组件的思路就是将弹窗组件渲染到根节点的子节点内
    // 如果根组件就只有一个根节点且是不能增加子节点的节点例如RouterView时，弹窗组件加入子节点只会导致RouterView不能工作
    // 解决方案：手动渲染一个Fragment
    if (!vnode.children)
      vnode = h(Fragment, [vnode]);

    const newvnode = render2.call(arguments[0], vnode);
    if (newvnode)
      vnode = newvnode;
    return vnode;
  }
  // 运行时注入时，优先于render的beforeCreate和beforeMount都可以成功替换render
  // 创建组件实例时，render函数是赋值的component.render，替换后每次创建组件实例都是新的render函数
  // 但是组件第一次被创建时，组件实例已经被生成，所以要替换掉该实例的render函数
  if (instance)
    instance.$.render = component.render;
}

/** VUE的规则是深度优先，继承优先 */
function componentWalker(component, func) {
  if (component.extends)
    componentWalker(component.extends, func);
  if (component.mixins)
    for (const mixin of component.mixins)
      componentWalker(mixin, func);
  func(component);
}